﻿#include "stdafx.h"
#include "resource.h"
#include "Globals.h"
#include <Userenv.h>

#pragma comment(lib, "Userenv.lib")

std::vector<
std::vector<OsuFile*> >				PTT::OsuFiles;			// Parsed osu files

std::vector<std::wstring>			PTT::SongNames;		// artist + name of songs

Index										PTT::NameIndex;		// Alphabetical index of "SongNames"

Window3D									*PTT::wnd				= NULL;

Audio										*PTT::audio				= NULL;

std::wstring							PTT::DrumFiles[4]		= {L""};

MediaFile								*PTT::drum[4]			= {NULL};
Texture									*PTT::texCursor		= NULL;
GraphicsFile							*PTT::defaultBG		= NULL;

const
TiledTexture							*PTT::NumbersSmall	= NULL,
											*PTT::NumbersBig		= NULL,
											*PTT::FixedFont		= NULL,
											*PTT::Symbols			= NULL,
											*PTT::Bubble			= NULL,
											*PTT::SunFlare			= NULL,
											*PTT::Buttons1			= NULL,
											*PTT::Buttons2			= NULL;

//----
//	This song gets selected during "SelectSong", is used by "PreviewThread", and finally can be played with "PlaySong"
const OsuFile							*PTT::SelectedSong	= NULL;
bool										PTT::BonusMode			= false;
double									PTT::Quality			= 0.0;
bool										PTT::UseAudio			= false;

//----
bool										PTT::Fullscreen		= false;
int										PTT::MasterVolume		= 100,	// can be changed during PlaySong
											PTT::SongVolume		= 100,	// whoever plays a song, is responsible to set this volume!
											PTT::EffectsVolume[4]= {100};	// whoever plays a sound effect, is responsible to set this volume!
std::wstring							PTT::SongsDir;						// osu songs folder

DWORD										PTT::VK_BUTTONS[4]	= {VK_RIGHT,VK_DOWN,VK_LEFT,VK_UP};
DWORD										PTT::ms_error_count[1024];
double									PTT::ms_error_mode	= 0.0;

std::vector<PTT::HighscoreEntry>	PTT::Highscores;					// high score table


//-----------------------------------------------------------------------------
//	Helper functions
	void ReadConfig();
	void FindAndReadOsuFiles();
	void LoadHighscores();
	void SaveHighscores();
	void LoadErrors();
	void LoadButtons();

//-----------------------------------------------------------------------------
void PTT::InitGlobals()
{
//----
	wnd = Window3D::CreateWindowAndThread();
	wnd->handleMouseEvent = Input::HandleMouseEvent;
	wnd->handleKeyEvent   = Input::HandleKeyEvent;

	SetWindowText(wnd->getHWND(), L"Project Tea Time");

//----
	ReadConfig();

	FindAndReadOsuFiles();

	LoadHighscores();
	LoadButtons();
	LoadErrors();

	if(Fullscreen)	wnd->switchFullscreen(true);
	else				wnd->resize(1280,720);
	wnd->initialize();

//----
	audio				= new Audio;
	texCursor		= new Texture(L"graphics/cursor.png");
	NumbersSmall	= new TiledTexture(L"graphics/numbers_small.png", L'0', 10, 1, 0.8f);
	NumbersBig		= new TiledTexture(L"graphics/numbers_big.png");
	FixedFont		= new TiledTexture(L"graphics/charset2_small.png", L' ', 91, 1, 0.7f);
	Symbols			= new TiledTexture(L"graphics/symbols.png", L'★', 3);
	Bubble			= new TiledTexture(L"graphics/bubble.png", 0, 1);		// just one frame
	SunFlare			= new TiledTexture(L"graphics/sun.png",    0, 1);		// just one frame
	Buttons1			= new TiledTexture(L"graphics/buttons1.png",		0, 4);// these are indexed by integers [0,3]
	Buttons2			= new TiledTexture(L"graphics/buttons3.png",		0, 4),
	defaultBG		= new GraphicsFile(L"graphics/bg.png");
	drum[0]			= new MediaFile	(audio, DrumFiles[0], 1);
	drum[1]			= new MediaFile	(audio, DrumFiles[1], 1);
	drum[2]			= new MediaFile	(audio, DrumFiles[2], 1);
	drum[3]			= new MediaFile	(audio, DrumFiles[3], 1);
/*	drum[0]			= new MediaFile	(audio, L"sound/normal-hitclap.wav",  1);
	drum[1]			= new MediaFile	(audio, L"sound/normal-hitclap2.wav", 1);
	drum[2]			= new MediaFile	(audio, L"sound/normal-hitclap3.wav", 1);
	drum[3]			= new MediaFile	(audio, L"sound/soft-hitclap.wav",    1);
*/
}


//-----------------------------------------------------------------------------
void PTT::DeleteGlobals()
{
	for(int i = 0; i < OsuFiles.size(); i++)
	for(int j = 0; j < OsuFiles[i].size(); j++)
		delete OsuFiles[i][j];

	delete drum[3];
	delete drum[2];
	delete drum[1];
	delete drum[0];
	delete defaultBG;
	delete FixedFont;
	delete NumbersBig;
	delete NumbersSmall;
	delete texCursor;
	delete audio;
}



//-----------------------------------------------------------------------------
std::wstring UserDirectory()
{
	HANDLE processHandle;

	std::wstring ret;

	if(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &processHandle))
	{
		DWORD strSize;

		GetUserProfileDirectory(processHandle, NULL, &strSize);

		ret.resize(strSize);

		GetUserProfileDirectory(processHandle, &ret[0], &strSize);

		if(strSize > 0)
			ret.resize(strSize-1);			// remove terminating null character

		CloseHandle(processHandle);
	}

	return ret;
}

//-----------------------------------------------------------------------------
DWORD hash(const std::wstring &s, DWORD start = 0)
{
	for(unsigned i = 0; i < s.size(); i++)
		start = start * 101 + s[i];
	return start;
}


//-----------------------------------------------------------------------------
unsigned GetHighscoreIndex()
{
	if(PTT::SelectedSong==NULL)
		return 0;

//----
//	Get the current songs hash key, from the metadata
	const OsuFile::Metadata &m = PTT::SelectedSong->metadata;
	DWORD key = 0;
	key = hash(m.title);
	key = hash(m.artist, key);
	key = hash(m.creator,key);
	key = hash(m.version,key);
	key = hash(m.source, key);
	key = hash(m.tags,   key);

//----
//	Search the table for our key
	std::vector<PTT::HighscoreEntry> &H = PTT::Highscores;

	unsigned n;
	for(n = 0; n < H.size(); n++)
	{
		if(H[n].key == key)
			return n;
	}

//----
//	Need to add a new entry
	PTT::HighscoreEntry newEntry = {key,0};
	H.push_back(newEntry);
	return n;
}

//-----------------------------------------------------------------------------
void PTT::SetHighscore(DWORD score, bool great, bool perfect)
{
	unsigned index = GetHighscoreIndex();

	bool save = false;

	if(PTT::BonusMode)
	{
		if(PTT::Highscores[index].score2 < score)
		{
			PTT::Highscores[index].score2 = score;
			save = true;
		}

		if((PTT::Highscores[index].great & 0x2) == 0 && great)
		{
			PTT::Highscores[index].great |= 0x2;
			save = true;
		}

		if((PTT::Highscores[index].great & 0x8) == 0 && perfect)
		{
			PTT::Highscores[index].great |= 0x8;
			save = true;
		}
	}
	else
	{
		if(PTT::Highscores[index].score1 < score)
		{
			PTT::Highscores[index].score1 = score;
			save = true;
		}

		if((PTT::Highscores[index].great & 0x1) == 0 && great)
		{
			PTT::Highscores[index].great |= 0x1;
			save = true;
		}

		if((PTT::Highscores[index].great & 0x4) == 0 && perfect)
		{
			PTT::Highscores[index].great |= 0x4;
			save = true;
		}
	}

	if(save)
		SaveHighscores();
}


//-----------------------------------------------------------------------------
void PTT::GetHighscore(DWORD &score1, DWORD &score2, DWORD *great)
{
	if(PTT::Highscores.size())
	{
		unsigned index = GetHighscoreIndex();
		score1 = PTT::Highscores[index].score1;
		score2 = PTT::Highscores[index].score2;

		if(great)
			*great = PTT::Highscores[index].great;
	}
	else
	{
		score1 = 0;
		score2 = 0;
	}
}

//-----------------------------------------------------------------------------
void PTT::SaveErrors()
{
	std::wstring	filename(UserDirectory()+L"\\errors.ptt");
	std::ofstream	file(filename, std::ios::out | std::ios::binary);

	if(file.good())
		file.write((const char*) PTT::ms_error_count, sizeof(PTT::ms_error_count));

	file.close();
}

//-----------------------------------------------------------------------------
void LoadErrors()
{
	std::wstring	filename(UserDirectory()+L"\\errors.ptt");
	std::ifstream	file(filename, std::ios::in | std::ios::binary);

	if(file.good())
		file.read ((char*) PTT::ms_error_count, sizeof(PTT::ms_error_count));

	file.close();
}

//-----------------------------------------------------------------------------
void SaveHighscores()
{
	std::wstring	filename(UserDirectory()+L"\\save.ptt");
	std::ofstream	file(filename, std::ios::out | std::ios::binary);

	if(file.good())
	{
		PTT::Header header;
		memset(&header,0,sizeof(header));
		header.id = PTT::Header::PTT_;
		header.version = 2;
		header.sizeHighscores	= sizeof(PTT::Highscores[0]) * PTT::Highscores.size();

		file.write((const char*) &header, sizeof(header));
		file.write((const char*) &PTT::Highscores[0], header.sizeHighscores);
	}

	file.close();
}

//-----------------------------------------------------------------------------
void LoadHighscores()
{
	std::wstring	filename(UserDirectory()+L"\\save.ptt");
	std::ifstream	file(filename, std::ios::in | std::ios::binary);

	if(file.good())
	{
		std::vector<PTT::HighscoreEntry> &H = PTT::Highscores;

		file.seekg(0,std::ios::end);
		unsigned	filesize = unsigned(file.tellg());
		file.seekg(std::ios::beg);

		if(filesize)
		{
			PTT::Header header;
			memset(&header,0,sizeof(header));
			if(filesize >= sizeof(PTT::Header))
			{
				file.read ((char*)&header, sizeof(header));
			//----
			//	Version 0 doesn't have a header
				if(header.id != PTT::Header::PTT_)
				{
					memset(&header, 0, sizeof(header));
					file.seekg(std::ios::beg);
				}
			}

			if(header.version == 0)
			{
				H.resize(filesize / (2*sizeof(DWORD)));
				for(unsigned i = 0; i < H.size(); i++)
				{
					file.read ((char*)&H[i], 2*sizeof(DWORD));
					H[i].score2	= 0;
					H[i].great	= 0;
				}
				memset(PTT::ms_error_count, 0, sizeof(PTT::ms_error_count));
			}
			else if(header.version == 1)
			{
				H.resize((filesize-sizeof(PTT::Header)) / sizeof(PTT::HighscoreEntry));
				file.read ((char*)&H[0], H.size()*sizeof(PTT::HighscoreEntry));
				memset(PTT::ms_error_count, 0, sizeof(PTT::ms_error_count));
			}
			else if(header.version == 2)
			{
				H.resize(header.sizeHighscores / sizeof(PTT::HighscoreEntry));
				file.read ((char*)&H[0], header.sizeHighscores);
//				file.read ((char*)PTT::ms_error_count, header.reserved[0]);
			}
		}
	}

	file.close();
}


//-----------------------------------------------------------------------------
void PTT::SaveButtons()
{
	std::wstring	filename(UserDirectory()+L"\\config.ptt");
	std::ofstream	file(filename, std::ios::out | std::ios::binary);

	if(file.good())
		file.write((const char*) PTT::VK_BUTTONS, sizeof(PTT::VK_BUTTONS));

	file.close();
}

//-----------------------------------------------------------------------------
void LoadButtons()
{
	std::wstring	filename(UserDirectory()+L"\\config.ptt");
	std::ifstream	file(filename, std::ios::in | std::ios::binary);

	if(file.good())
		file.read ((char*) PTT::VK_BUTTONS, sizeof(PTT::VK_BUTTONS));

	file.close();
}


//-----------------------------------------------------------------------------
//	Gets the next not line which is not empty or a comment
void GetNextLine(std::wifstream &file, std::wstring &buffer)
{
	while(!file.eof())
	{
		getline(file, buffer);

		if(buffer.size()!=0 && buffer[0]!=L'#')
			return;
	}
}

//-----------------------------------------------------------------------------
void ReadConfig()
{
//----
//	open unicode file
	std::wifstream file;
	std::locale utf8loc(std::locale(), new utf8::utf8_codecvt_facet());
	file.imbue(utf8loc);
	file.open(L"config.ini");

//----
	static const
	std::wstring	osu_files		(L"[osu files]"),
						fullsreen		(L"[fullscreen]"),
						master_volume	(L"[master volume]"),
						song_volume		(L"[song volume]"),
						effects_volume	(L"[effects volume]"),
						effects_files	(L"[effects files]");
	std::wstring	buffer;

	while(!file.eof())
	{
		GetNextLine(file,buffer);

AGAIN:

		if(buffer == fullsreen)
		{
			GetNextLine(file,buffer);
			if(buffer[0]!=L'0' && buffer[0]!=L'f')			// "0" or "false"
				PTT::Fullscreen = true;
		}
		else if(buffer == master_volume)
		{
			GetNextLine(file,buffer);
			PTT::MasterVolume = _wtoi(&buffer[0]);
		}
		else if(buffer == song_volume)
		{
			GetNextLine(file,buffer);
			PTT::SongVolume = _wtoi(&buffer[0]);
		}
		else if(buffer == effects_volume)
		{
			for(int i = 0; i < 4; i++)
			{
				GetNextLine(file,buffer);
				if(buffer[0] != L'[')
					PTT::EffectsVolume[i] = _wtoi(&buffer[0]);
				else
					goto AGAIN;		
			}
		}
		else if(buffer == effects_files)
		{
			for(int i = 0; i < 4; i++)
			{
				GetNextLine(file, buffer);
				if(buffer[0] != L'[')
					PTT::DrumFiles[i] = buffer;
				else
					goto AGAIN;
			}
		}
		else if(buffer.find(osu_files)!=std::wstring::npos)
		{
			GetNextLine(file, PTT::SongsDir);
			wchar_t lastChar = PTT::SongsDir[PTT::SongsDir.size()-1];
			if(lastChar != L'\\'
			&& lastChar != L'/')
				PTT::SongsDir += L'\\';
		}
	}

//----
//	get osu! song directory from registry?
	if(PTT::SongsDir == L"from_registry\\")
	{
		HKEY keyHandle;
		LONG result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"Software\\osu!\\", 0, KEY_QUERY_VALUE|KEY_WOW64_32KEY, &keyHandle);
		DWORD size = 64;


		if(result==ERROR_SUCCESS)
		{	do
			{
				PTT::SongsDir.resize(size/2);
				result = RegGetValue(keyHandle, NULL, L"Path", RRF_RT_REG_SZ, NULL, &PTT::SongsDir[0], &size);
			} while(result==ERROR_MORE_DATA);
			PTT::SongsDir.resize(size/2 - 1);		// clear the terminating NULL character
			PTT::SongsDir += L"\\Songs\\";
		}
		else
		{
			std::cout << result << std::endl;
			Error(L"Couldn't find osu! installation.\nPlease specify song directory in config.ini.");
		}

		RegCloseKey(keyHandle);
	}

	file.close();
}



//-----------------------------------------------------------------------------
// string with case insensitive comparison.
struct StrCmpNoCase : public std::wstring
{
	void operator = (const std::wstring &s)	{ std::wstring::operator=(s);	}
	bool operator < (const StrCmpNoCase &b) const
	{
		const StrCmpNoCase &a = *this;
		for(unsigned i = 0; i < a.length() && i < b.length(); i++)
		{
			if			(tolower(a[i]) < tolower(b[i]))	return true;
			else if	(tolower(a[i]) > tolower(b[i]))	return false;
		}

		return a.length() < b.length();
	}
};



//-----------------------------------------------------------------------------
void FindAndReadOsuFiles()
{
	using namespace PTT;

	std::vector<std::wstring>	SongFolders,	// subfolders of the songs directory
										SongFoldersAlt;// alternate names

//----
//	Find all files (usually subfolders) in "*.osu!/Songs/"
	WIN32_FIND_DATA findData;
	HANDLE hSearch = FindFirstFile((SongsDir+L"*").c_str(), &findData);

	if(hSearch!=INVALID_HANDLE_VALUE)
	{
		FindNextFile(hSearch, &findData);					// == ".."

		while(FindNextFile(hSearch, &findData))
		{
			SongFolders.push_back(findData.cFileName);
			SongFoldersAlt.push_back(findData.cAlternateFileName[0] ? findData.cAlternateFileName : findData.cFileName);
		}
	}
	else
	{
		SongFolders.push_back(L"Couldn't find osu songs folder: \""+SongsDir+L"\"\nPlease check config.ini");
		SongFoldersAlt.push_back(L"");
	}

	FindClose(hSearch);


//----
	std::wstring songDir;

//----
//	Find and parse all *.osu files in every song folder
	OsuFiles.resize(0);
	OsuFiles.reserve(SongFolders.size());

//	BENCH_START(1)
	for(unsigned i = 0; i < SongFoldersAlt.size(); i++)
	{
		songDir = SongsDir+SongFoldersAlt[i]+L'\\';

		hSearch = FindFirstFile((songDir+L"*.osu").c_str(), &findData);

		bool foundFile = hSearch!=INVALID_HANDLE_VALUE;

		if(foundFile)
		{
			do
			{
				if(OsuFiles.size()<i+1)
					OsuFiles.resize(i+1);

				OsuFile *o = new OsuFile;
				OsuFiles[i].push_back(o);

			//----
			//	read file and parse data
				o->readOsuFile((songDir+findData.cFileName).c_str());

			//----
			//	append full path to filenames, if it's not empty
				if(o->audioFilename.size())
					o->audioFilename			= songDir + o->audioFilename;
				if(o->videoFilename.size())
					o->videoFilename			= songDir + o->videoFilename;
				if(o->backgroundFilename.size())
					o->backgroundFilename	= songDir + o->backgroundFilename;

			} while(FindNextFile(hSearch, &findData));
		}

		FindClose(hSearch);

	//----
	//	find *.mp3 and *.ogg files and create dummy osu objects for them
		if(!foundFile)
		{
			hSearch = FindFirstFile((songDir+L"*.mp3").c_str(), &findData);
			foundFile = hSearch!=INVALID_HANDLE_VALUE;
			if(!foundFile)
			{
				FindClose(hSearch);
				hSearch = FindFirstFile((songDir+L"*.ogg").c_str(), &findData);
				foundFile = hSearch!=INVALID_HANDLE_VALUE;
			}
			if(!foundFile)
			{
				FindClose(hSearch);
				hSearch = FindFirstFile((songDir+L"*.m4a").c_str(), &findData);
				foundFile = hSearch!=INVALID_HANDLE_VALUE;
			}
			if(!foundFile)
			{
				FindClose(hSearch);
				hSearch = FindFirstFile((songDir+L"*.wav").c_str(), &findData);
				foundFile = hSearch!=INVALID_HANDLE_VALUE;
			}
			if(!foundFile)
			{
				FindClose(hSearch);
				hSearch = FindFirstFile((songDir+L"*.mkv").c_str(), &findData);
				foundFile = hSearch!=INVALID_HANDLE_VALUE;
			}

		//----
		//	Maybe it wasn't a folder after all but an audio file?
			const wchar_t	*filename  = findData.cFileName,
								*alternate = findData.cAlternateFileName;
			std::wstring	fn, afn;
			bool foundVideo = false;
			if(!foundFile)
			{
				std::wstring::size_type k = SongFolders[i].find_last_of(L'.');
				if(k!=-1)
				{
					std::wstring ext = SongFolders[i].substr(k+1);
					if(ext == L"mp3" || ext==L"ogg" || ext==L"m4a" || ext==L"wav" || ext==L"mkv")
					{
						if(ext == L"mkv")
							foundVideo = true;
						songDir.resize(songDir.find_last_of(L'\\', songDir.size()-2)+1);
						fn = SongFolders[i];
						afn = SongFoldersAlt[i];
						filename = fn.c_str();
						alternate = afn.c_str();
						foundFile = true;
					}
				}
			}

			if(foundFile)
			{
				do
				{
					if(OsuFiles.size()<i+1)
						OsuFiles.resize(i+1);

					OsuFile *o = new OsuFile;
					OsuFiles[i].push_back(o);

				//----
				//	set full path to file
					o->audioFilename		= songDir + alternate;
					o->metadata.version	=           filename;
					o->metadata.artist	= SongFolders[i];
					o->metadata.title		= filename;
					o->metadata.title.resize(o->metadata.title.find_last_of(L'.'));
					o->overallDifficulty	= -1;
					if(foundVideo)
						o->videoFilename	= o->audioFilename;

				} while(FindNextFile(hSearch, &findData));
			}

			FindClose(hSearch);
		}
	}

//	BENCH_STOP

//----
//	Sort osu files; not very efficient, as we have to copy the several KB large OsuFile structs,
//	but the file parsing is ~10x slower and everything's done only once anyway (affects startup time).
	for(unsigned i = 0; i < OsuFiles.size(); i++)
	{
		std::sort(OsuFiles[i].begin(), OsuFiles[i].end(), [](const OsuFile *x, const OsuFile *y){return (*x) < (*y);});
	}

//----
//	Make "artist - title" strings and index them alphabetically
	std::vector<StrCmpNoCase> cmpNames(OsuFiles.size());
	SongNames.resize(OsuFiles.size());
	for(unsigned i = 0; i < OsuFiles.size(); i++)
	{
		if(OsuFiles[i].size())
		{
			OsuFile *o = OsuFiles[i][0];
			if(o->metadata.artist.size())	SongNames[i] = o->metadata.artist+L" - ";
			else									SongNames[i] = L" ";
													SongNames[i] += o->metadata.title;
		}
		else
			SongNames[i] = SongFolders[i] + L" (no *.osu file found!)";

		cmpNames[i] = SongNames[i];
	}

	NameIndex.index(cmpNames);

}

